Tutorial / Add enemies
Add enemies We can move, turn and shoot, now, we'll add enemies to shoot at! The addition of enemies in the game will be the same as for our shots: we will define an enemy and his behavior in a prefabricated scene and then it this scene in our main scene when necessary. Creation of our prefab enemy Start by creating a new scene called "Prefab_enemy" add an object at (0,0,0), associate an enemy model (again in my case a cube with a different texture) and finally associate a new script named "enemy_IA." This will be our enemy artificial intelligence. Instead of a real intelligence,our enemy will always move toward the player. In the script enemy_IA's Awake(), let's get our player object : self.target = CraftStudio.FindGameObject( "Cube" ) We define the enemy movement speed : self.speed = 0.3 Then in update() we will just make the enemy face the player and move in this direction : local targetPos = self.target.transform:GetPosition() self.gameObject.transform:LookAt( targetPos ) self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) Enemies spawning It remains to spawn our enemies: In the main "game" scene, add a new script "spawn_enemies" linked to the object "map".In Awake() we will create a new object "Enemies" that will contains all the enemies that we will create later. It will allows us to easily find them later : self.ennemiesRoot = CraftStudio.CreateGameObject( "Ennemies" ) Then, in Update() we create a new enemy linked to the root object : CraftStudio.AppendScene( CraftStudio.FindAsset( "Prefab_enemy" ), self.ennemiesRoot ) This will spawn an enemy every game cycle... it will be a lot ! We will add a delay to spawn one just every 5 seconds : function Behavior:Awake() self.ennemiesRoot = CraftStudio.CreateGameObject( "Enemies" ) self.spawntimer = 0 end function Behavior:Update() self.spawntimer = self.spawntimer+1 if self.spawntimer 60*5 then CraftStudio.AppendScene( CraftStudio.FindAsset( "Prefab_enemy" ), self.ennemiesRoot ) self.spawntimer = 0 end end Start your game and test it. After 5 seconds, an enemy should appear .... in the ground, always in the same place, then move towards the player and follow him. We will change the creation of our enemies so that they always appear at some distance of the player. In awake(), define a spawning distance : self.spawnDist = 10 Then in Update(), we get the player's position: local TargetPos = self.target.transform:GetPosition() And define the position where it appear by shifting the of the player position to the north. Then reposition our enemy : self.spawPos= TargetPos + Vector3:Forward() * self.spawnDist self.gameObject.transform:SetPosition(self.spawPos) Launch, test... Enemies now appear always at the same position relative to the player (north since we used Vector3:Forward ().) We will add some randomization using a random number generator. The generator is part of the object "math" and needs to be initialized by the method math.randomseed(seed). The seed is a value from which the next numbers will be generated. Same seed always gives the same sequence of numbers (our generator is actually "pseudo-random") In order to have a different game every time, we use the time being as seed: math.randomseed (os.time()) We will use our generator to select a random angle between 0 and 360 degrees local angle = math.random(0, 359) Then we rotate the forward vector by the our angle found : Vector3.Transform(Vector3:Forward(), Rotation Quaternion) The rotation quaternion is a mathematical object that describes the rotation. We can create It from the rotation angle and the axis around which rotate (Y axis upwards): Quaternion:FromAxisAngle(Vector3:Up(), angle) The starting position is : self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * SPAWN_DISTANCE Full Script : spawn_enemies : function Behavior:Awake() math.randomseed(os.time()) self.enemiesRoot = CraftStudio.CreateGameObject( "Enemies" ) self.spawntimer = 0 end function Behavior:Update() self.spawntimer = self.spawntimer+1 if self.spawntimer 60*5 then CraftStudio.AppendScene( CraftStudio.FindAsset( "prefab_enemy" ), self.enemiesRoot ) self.spawntimer = 0 end end Enemy_AI : function Behavior:Awake() self.speed = 0.03 self.target = CraftStudio.FindGameObject( "Cube" ) --target --spawning self.spawnDist = 10 --distance d'apparition local angle = math.random( 0, 359 ) local targetPos = self.target.transform:GetPosition() self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist self.gameObject.transform:SetPosition( self.spawPos ) end function Behavior:Update() --gestion du mouvement local targetPos = self.target.transform:GetPosition() self.gameObject.transform:LookAt( targetPos ) self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) end Kill the enemies ... Now that our enemies are alive, we'll have to shoot them! For that, we will need to determine when a bullet hits an enemy, and then destroy the bullet and the enemy. We will ensure that this is the bullet that detects a collision with an enemy, but we could all do quite the opposite. There are several techniques more or less complicated to detect collisions between two objects. Here we use the simplest and least accurate: the distance detection. In our script "bullet", we will parse all existing enemies. For each enemy, we will check how far it is from the bullet. In Update(), starts by retrieve the list of enemies present, remember that we have deliberately placed then in the same parent object "Enemies." We recover the children of the object "Enemies" with : local enemies = CraftStudio.FindGameObject( "Enemies" ):GetChildren() Then we also retrieve the actual position of the bullet : local pos = self.gameObject.transform:GetPosition() We will us a "for" statement to loop trough our list of enemies : for i=1,#enemies do local enemy = enemiesi end In the loop, for each enemies, we calculate the distance between the bullet and the enemy (enemy.transform:GetPosition()) with : Number Vector3.Distance( Vector3 v1, Vector3 v2 ) It actually gives us us the distance between the center of both object (assuming your enemy is more or less square and his origin is at his center). If this distance is less than the radius of the enemy, then we will consider it touched. In my case, enemy is a cube of side 1, so I will consider its radius equal to 0.5. (Obviously collisions obtained will not be perfect, but it's a start...) We therefore test our collision with: if Vector3.Distance( enemy.transform:GetPosition(), pos ) < 0.5 then end And finally, if the collision occur, we just destroy the bullet : CraftStudio.Destroy( self.gameObject ) We now need to destroy the smitten enemy. We could actually do it from the actual script (the one attached to the projectile), but as a principle, it's best to modify an object only from a script attached to it. This rule is obviously not absolute, but it will allow manage thing easily when your project will get bigger. So, we will destroy our enemy from the script "Enemy_IA". To do it, in this one we will create a new function (a "method" to be exact): function Behavior:Damage( ) end This function define what happens when our enemy is hit by a bullet. For the time being, we will just destroy our enemy: CraftStudio.Destroy( self.gameObject ) The ''Damage() ''method can be called within his own script "Enemy_IA" with self:Damage() or from within an other script with : GameObject:SendMessage( string method name, table data ) Here, the used object will be the smitten enemy, the method name will be "Damage" and the data should be what we want to pass to this method as parameter (nothing here, but it could be used to the amount of damage for a particular weapon for example ). This way we get : enemy:SendMessage( "Damage",nil) And so the enemy's final script : function Behavior:Awake() self.speed = 0.03 self.target = CraftStudio.FindGameObject( "Cube" ) --cible à suivre --spawning position self.spawnDist = 10 --distance d'apparition math.randomseed(os.time()) local angle = math.random( 0, 359 ) local targetPos = self.target.transform:GetPosition() self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist self.gameObject.transform:SetPosition( self.spawPos ) end function Behavior:Update() --handle movement local targetPos = self.target.transform:GetPosition() self.gameObject.transform:LookAt( targetPos ) self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) end --Method called when a bullet collide the enemy function Behavior:Damage( ) print ("ai") CraftStudio.Destroy( self.gameObject ) end Bullet'script : function Behavior:Awake() self.speed = 0.5 self.MaxLife = 20 self.source = CraftStudio.FindGameObject( "Cube" ) --starting position self.gameObject.transform:SetPosition (self.source.transform:GetPosition()) self.gameObject.transform:SetOrientation (self.source.transform:GetOrientation()) --life initialization self.life = 0 end function Behavior:Update() --handle movments self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) --handle life span self.life = self.life +1 if self.life > self.MaxLife then CraftStudio.Destroy( self.gameObject ) end --handle colisions with enemies local enemies = CraftStudio.FindGameObject( "Enemies" ):GetChildren() local pos = self.gameObject.transform:GetPosition() for i=1,#enemies do local enemy = enemiesi if Vector3.Distance( enemy.transform:GetPosition(), pos ) < 0.5 then enemy:SendMessage( "Damage",nil) CraftStudio.Destroy( self.gameObject ) end end end Earn points Edit Now that we can destroy enemies, we will add a point management: In the character's management script, we add a variable that will store our score: self.score = 0 We will also create a method to add points. It takes as a parameter the number of points to add and simply add it to the current score and then display it in the debug window: function Behavior:AddPoint(point) self.score = self.score + point print ("You have "..self.score.." points") end It remains to define when AddPoint() will be called. It will be our enemies at the time of death which will give the player points. So in the "Ennemi_AI" script, add a script property (in the banner at the top of the script) in order to define the number of point given to the player. Call it "ScoreWhenKilled", defined as a number with 1 as default. Then in the function Damage() in "Ennemi_AI" script, add: self.target:SendMessage("AddPoint", self.ScoreWhenKilled) The contents of the property ScoreWhenKill, will be passed as a parameter to "AddPoint()" and thus added to the player's score. And get killed... Now that we can kill the enemies, we'll make sure that they can also killed us. We will use the same principle as for collisions between projectiles and enemies. In "ennemi_AI" script, Update() will test collisions with the player in the sae we did for the bullet: local direction = targetPos - self.gameObject.transform:GetPosition() local distance = direction:Length() if distance < 1 then self.target:SendMessage( "Damage",nil) CraftStudio.Destroy( self.gameObject ) end Then, we create a new "Damage()" method in the player script to handle what happen when an enemy touch the player : function Behavior:Damage( ) CraftStudio.LoadScene (CraftStudio.FindAsset("menu")) end Run your game and check that, if the enemy touches you, the game go back to the main menu.It's a little rough right? We will add a life bar to our character: in the player's script, in awake() add : self.life = 10 In "ennemi_AI" script we will define how many hit damage cause the enemy when it touches the player. In order to change this value easily if you create several different types of enemies, we will create as script property. Create a property in script "ennemi_AI" named "power", it will be a number with 1 as default.In this same script change the called line when a collision with the player accur to have: self.target:SendMessage( "Damage",self.power) We will now change the player's "Damage()" method to reduce the player life level each time it collides an enemy. At the beginnig of the method, we add : self.life = self.life -damage We also add a line to show the remaining life point in the debug window : print ("You still have "..self.life.."life points") Et finelly we get back to the menu when the player is out of life points :: if self.life <= 0 then CraftStudio.LoadScene (CraftStudio.FindAsset("menu")) end Final Script : Player : function Behavior:Awake() self.speed = 0.05 self.life = 10 self.score = 0 self.groundPlane = Plane:New( Vector3:Up(), -1 ) self.crosshair = CraftStudio.FindGameObject( "crosshair" ) self.cameraComponent = CraftStudio.FindGameObject( "Camera" ):GetComponent( "Camera" ) self.bulletPrefab = CraftStudio.FindAsset( "prefab_bullet" ) end function Behavior:Update() --handle movement local horizontal = CraftStudio.Input.GetAxisValue( "Horizontal" ) local vertical = CraftStudio.Input.GetAxisValue( "Vertical" ) local movement = Vector3:New( horizontal, 0, -vertical ) movement = movement * self.speed self.gameObject.transform:Move (movement) --handle orientation local mousePos = CraftStudio.Input.GetMousePosition() local mouseRay = self.cameraComponent:CreateRay( mousePos ) local distance = mouseRay:IntersectsPlane( self.groundPlane ) if distance ~= nil then local targetPos = mouseRay.position + mouseRay.direction * distance self.crosshair.transform:SetPosition( targetPos ) self.gameObject.transform:LookAt( targetPos ) end --handle firing if CraftStudio.Input.IsButtonDown( "Fire" ) then CraftStudio.Instantiate("bullet",self.bulletPrefab) end end --Method called when this object collide an enemy function Behavior:Damage(damage) self.life = self.life -damage print ("You still have "..self.life.."life points") if self.life <= 0 then CraftStudio.LoadScene (CraftStudio.FindAsset("menu")) end end --Method called when an enemy is killed function Behavior:AddPoint(point) self.score = self.score + point print ("You have "..self.score.." points") end Enemy : function Behavior:Awake() self.speed = 0.03 self.radius = 1 self.target = CraftStudio.FindGameObject( "Cube" ) --target --spawning self.spawnDist = 10 --distance d'apparition math.randomseed(os.time()) local angle = math.random( 0, 359 ) local targetPos = self.target.transform:GetPosition() self.spawPos = targetPos + Vector3.Transform( Vector3:Forward(), Quaternion:FromAxisAngle( Vector3:Up(), angle ) ) * self.spawnDist self.gameObject.transform:SetPosition( self.spawPos ) end function Behavior:Update() --movement local targetPos = self.target.transform:GetPosition() self.gameObject.transform:LookAt( targetPos ) self.gameObject.transform:MoveOriented( Vector3:Forward()*self.speed ) --check for collisions with player local direction = targetPos - self.gameObject.transform:GetPosition() local distance = direction:Length() if distance < 1 then self.target:SendMessage( "Damage",self.power) CraftStudio.Destroy( self.gameObject ) end end --Method called when a bullet collide function Behavior:Damage( ) self.target:SendMessage( "AddPoint",self.ScoreWhenKill) CraftStudio.Destroy( self.gameObject ) end